// The main gameplay section of the project.

const int FRAME_WIDTH  = 320;
const int FRAME_HEIGHT = 480;

class Game {

  ALuint buffers[256];

  enum {
	  STATE_INIT,
    STATE_SPLASH,
    STATE_MAIN_MENU,
    STATE_INSTRUCTIONS,
    STATE_MAIN_GAME,
    STATE_GAME_OVER,
    STATE_HIGH_SCORES
  };

  texture textures[NUMBER_OF_TEXTURES];
  
  vec4  scoreDisplayPoint_;
  vec4 backgroundColour;

  Sprite buildingSprite_;
  Sprite grassSprite_;
  Sprite splashScreenSprite_;
  Sprite mainMenuSprite_;
  Sprite gameOverSprite_;
  Sprite highScoresSprite_;
  Sprite balloonIconSprite_;
  Sprite instructionsSprite_;

  float ascent_;

  Balloon balloon_;
  Impulse impulse_;
  
  int topTen_[10];
  int distanceTravelled_;
  int state_;

  float scoreDigitHalfWidth_;
  float scoreDigitHalfHeight_;
  float widthRatio_;
  float heightRatio_;

  bool primed_;
  bool soundIsOn_;

  void loadTexture(int textureNumber, const char* fileName, int indentX, int indentY, int width, int height) {
    FILE *file = fopen(fileName, "rb");
    assert(file && fileName);
    fseek(file, 0, SEEK_END);
    size_t length = ftell(file);
    fseek(file, 0, SEEK_SET);
    char *tmp = (char*)malloc(length);
    fread(tmp, 1, length, file);
    
    textures[textureNumber].init(tmp, indentX, indentY, width, height);

    fclose(file);
    free(tmp);
  }

  void loadTextures() {

    // VERY INEFFICIENT!! WILL CHANGE THIS SOON
    loadTexture(TX_RED_BALLOON,              "assets/redballoon.tga",             0, 0,  64,  64);
    loadTexture(TX_BASKET,                   "assets/basket.tga",                 0, 0,  16,  16);
    loadTexture(TX_YELLOW_STAR,              "assets/yellowstar.tga",             0, 0, 256, 256);
    loadTexture(TX_RED_STAR,                 "assets/redstar.tga",                0, 0, 256, 256);
    loadTexture(TX_GREEN_STAR,               "assets/greenstar.tga",              0, 0, 256, 256);
    loadTexture(0,                           "assets/num0.tga",                   0, 0,  64,  64);
    loadTexture(1,                           "assets/num1.tga",                   0, 0,  64,  64);
    loadTexture(2,                           "assets/num2.tga",                   0, 0,  64,  64);
    loadTexture(3,                           "assets/num3.tga",                   0, 0,  64,  64);
    loadTexture(4,                           "assets/num4.tga",                   0, 0,  64,  64);
    loadTexture(5,                           "assets/num5.tga",                   0, 0,  64,  64);
    loadTexture(6,                           "assets/num6.tga",                   0, 0,  64,  64);
    loadTexture(7,                           "assets/num7.tga",                   0, 0,  64,  64);
    loadTexture(8,                           "assets/num8.tga",                   0, 0,  64,  64);
    loadTexture(9,                           "assets/num9.tga",                   0, 0,  64,  64);
    loadTexture(TX_BALLOON_ICON,             "assets/balloonicon.tga",            0, 0,  64,  64);
    loadTexture(TX_SPLASH_SCREEN,            "assets/splashscreen.tga",           0, 0, 512, 512);
    loadTexture(TX_HIGH_SCORES,              "assets/highscores.tga",             0, 0, 512, 512);
    loadTexture(TX_GAME_OVER,                "assets/gameover.tga",               0, 0, 512, 512);
    loadTexture(TX_MAIN_MENU,                "assets/mainmenu.tga",               0, 0, 512, 512);
    loadTexture(TX_START_BUTTON,             "assets/startbutton.tga",            0, 0, 128,  64);
    loadTexture(TX_START_BUTTON_OVER,        "assets/startbuttonover.tga",        0, 0, 128,  64);
    loadTexture(TX_HIGH_SCORES_BUTTON,       "assets/highscoresbutton.tga",       0, 0, 128,  64);
    loadTexture(TX_HIGH_SCORES_BUTTON_OVER,  "assets/highscoresbuttonover.tga",   0, 0, 128,  64);
    loadTexture(TX_CLOUD_1,                  "assets/cloud1.tga",                 0, 0, 256, 256);
    loadTexture(TX_CLOUD_2,                  "assets/cloud2.tga",                 0, 0, 256, 256);
    loadTexture(TX_CLOUD_3,                  "assets/cloud3.tga",                 0, 0, 256, 256);
    loadTexture(TX_CLOUD_4,                  "assets/cloud4.tga",                 0, 0, 256, 256);
    loadTexture(TX_FOREGRASS,                "assets/foregrass.tga",              0, 0, 512, 128);
    loadTexture(TX_BUILDING,                 "assets/building.tga",               0, 0, 512, 512);
    loadTexture(TX_STARRY_NIGHT,             "assets/starrynight.tga",            0, 0, 512, 512);
    loadTexture(TX_SOUND_IS_ON,              "assets/soundison.tga",              0, 0,  32,  32);
    loadTexture(TX_SOUND_IS_ON_OVER,         "assets/soundisonover.tga",          0, 0,  32,  32);
    loadTexture(TX_SOUND_IS_OFF,             "assets/soundisoff.tga",             0, 0,  32,  32);
    loadTexture(TX_SOUND_IS_OFF_OVER,        "assets/soundisoffover.tga",         0, 0,  32,  32);
    loadTexture(TX_INSTRUCTIONS,             "assets/instructions.tga",           0, 0, 512, 512);
    loadTexture(TX_INSTRUCTIONS_BUTTON,      "assets/instructionsbutton.tga",     0, 0, 128,  64);
    loadTexture(TX_INSTRUCTIONS_BUTTON_OVER, "assets/instructionsbuttonover.tga", 0, 0, 128,  64);
  }

  void initGame() {
    impulse_.on = false;

    ascent_ = 0;
    distanceTravelled_ = 0;

    state_ = STATE_SPLASH;

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 

    StarManager::get().init(FRAME_WIDTH / 2, FRAME_HEIGHT / 2, -FRAME_WIDTH / 2, -FRAME_HEIGHT / 2);
    CloudManager::get().startup(vec4(-FRAME_WIDTH / 2, FRAME_HEIGHT / 2, FRAME_WIDTH / 2, -FRAME_HEIGHT / 2));

    scoreDigitHalfWidth_ = 12;
    scoreDigitHalfHeight_ = 12;
    scoreDisplayPoint_ = vec4(0, 0, 0, 1);
	  primed_ = false;
    balloon_.setBounds(-FRAME_WIDTH / 2, FRAME_WIDTH / 2, FRAME_HEIGHT / 2, -FRAME_HEIGHT / 2);
    
    backgroundColour = vec4(0.6f, 0.8f, 0.9f, 1);
  }

  void enterMainMenuState() {
    Input::setButtonAsVisible(GUI_BUTTON_START, true);
    Input::setButtonAsVisible(GUI_BUTTON_INSTRUCTIONS, true);
    Input::setButtonAsVisible(GUI_BUTTON_HIGH_SCORES, true);
    if (soundIsOn_) {
      Input::setButtonAsVisible(GUI_BUTTON_SOUND_IS_ON, true);
    }
    else {
      Input::setButtonAsVisible(GUI_BUTTON_SOUND_IS_OFF, true);
    }

    state_ = STATE_MAIN_MENU;
  }

  void leaveMainMenuState(int state) {
    Input::setButtonAsVisible(GUI_BUTTON_START, false);
    Input::setButtonAsVisible(GUI_BUTTON_HIGH_SCORES, false);
    Input::setButtonAsVisible(GUI_BUTTON_INSTRUCTIONS, false);
    Input::setButtonAsVisible(GUI_BUTTON_SOUND_IS_ON, false);
    Input::setButtonAsVisible(GUI_BUTTON_SOUND_IS_OFF, false);

    state_ = state;
  }

  void reset() {
    StarManager::get().reset();
    CloudManager::get().reset();
    balloon_.reset();
    distanceTravelled_ = 0;
    backgroundColour = vec4(0.6f, 0.8f, 0.9f, 1);
  }

  void startGameOver() {
      state_ = STATE_GAME_OVER;
      Input::release(LEFT_MOUSE_BUTTON);
      int score = StarManager::get().getScore();
      if (score > topTen_[9]) {
        int index = 0;
        bool gotIndex = false;
        
        while (!gotIndex) {
          if (score > topTen_[index]) {
            gotIndex = true;
          }
          else {
            ++index;
          }
        }

        for (int i = 9; i > index; --i) {
          topTen_[i] = topTen_[i - 1];
        }
        topTen_[index] = score;
      }

      for (int i = 0; i != 10; ++i) {
        printf("%d\n", topTen_[i]);
      }
  }
  
  void update() {

    switch(state_) {

	  case STATE_INIT:
	    initGame();
	    break;
    case STATE_SPLASH:
	    if (!primed_) {
		    if (Input::isDown(LEFT_MOUSE_BUTTON)) {
			    primed_ = true;
		    }
	    }
	    else {
		    if (!Input::isDown(LEFT_MOUSE_BUTTON)) {
		      enterMainMenuState();
          primed_ = false;
		    }
	    }
      break;

    case STATE_MAIN_MENU:
      if (Input::isButtonEngaged(GUI_BUTTON_START)) {
        leaveMainMenuState(STATE_MAIN_GAME);
        Input::disengageButton(GUI_BUTTON_START);
      }

      else if (Input::isButtonEngaged(GUI_BUTTON_HIGH_SCORES)) {
        leaveMainMenuState(STATE_HIGH_SCORES);
        Input::disengageButton(GUI_BUTTON_HIGH_SCORES);
      }
      else if (Input::isButtonEngaged(GUI_BUTTON_INSTRUCTIONS)) {
        leaveMainMenuState(STATE_INSTRUCTIONS);
        Input::disengageButton(GUI_BUTTON_INSTRUCTIONS);
      }

      else if (Input::isButtonEngaged(GUI_BUTTON_SOUND_IS_ON)) {
        soundIsOn_ = false;
        Input::disengageButton(GUI_BUTTON_SOUND_IS_ON);
        Input::setButtonAsVisible(GUI_BUTTON_SOUND_IS_ON, false);
        Input::setButtonAsVisible(GUI_BUTTON_SOUND_IS_OFF, true);
      }

      else if (Input::isButtonEngaged(GUI_BUTTON_SOUND_IS_OFF)) {
        soundIsOn_ = true;
        Input::disengageButton(GUI_BUTTON_SOUND_IS_OFF);
        Input::setButtonAsVisible(GUI_BUTTON_SOUND_IS_OFF, false);
        Input::setButtonAsVisible(GUI_BUTTON_SOUND_IS_ON, true);
      }


      break;

    case STATE_INSTRUCTIONS:
      if (!primed_) {
		    if (Input::isDown(LEFT_MOUSE_BUTTON)) {
			    primed_ = true;
		    }
	    }
	    else {
		    if (!Input::isDown(LEFT_MOUSE_BUTTON)) {
		      enterMainMenuState();
          primed_ = false;
		    }
	    }
      break;

    case STATE_MAIN_GAME:
        
      if (Input::isDown(LEFT_MOUSE_BUTTON)) {
        get().impulse_.setPosition(Input::getX(), Input::getY());
        get().impulse_.on = true;
      }

      if (!Input::isDown(LEFT_MOUSE_BUTTON)) {
        impulse_.setPosition(Input::getX(), Input::getY());
        impulse_.on = false;
      }        

      balloon_.update();
      if (balloon_.isTooLow()) {
        startGameOver();
      }
      if (StarManager::get().getLives() == 0) {
        startGameOver();
      }
      impulse_.update(&balloon_);
      ++distanceTravelled_;
      StarManager::get().update(&balloon_, distanceTravelled_, soundIsOn_);

      if (distanceTravelled_ > 2400) {
        backgroundColour[0] -= 0.0005;
        if (backgroundColour[0] < 0) backgroundColour[0] = 0;
        backgroundColour[1] -= 0.0005;
        if (backgroundColour[1] < 0) backgroundColour[1] = 0;
        backgroundColour[2] -= 0.0005;
        if (backgroundColour[2] < 0) backgroundColour[2] = 0;
      }

      if (distanceTravelled_ > 500) {
        CloudManager::get().update();
        // update clouds that are still visible, but no 'new' ones
        if (distanceTravelled_ > 3250) {  
          CloudManager::get().stopProducingClouds();
        }

        if (distanceTravelled_ > 4200) {
          // bring down the starry background until it fills the screen
        }
      }

      break;

    case STATE_GAME_OVER:
      if (!primed_) {
		    if (Input::isDown(LEFT_MOUSE_BUTTON)) {
			    primed_ = true;
		    }
	    }
	    else {
		    if (!Input::isDown(LEFT_MOUSE_BUTTON)) {
          state_ = STATE_HIGH_SCORES;
          reset();
          primed_ = false;
		    }
	    }
      break;

    case STATE_HIGH_SCORES:
      if (!primed_) {
		    if (Input::isDown(LEFT_MOUSE_BUTTON)) {
			    primed_ = true;
		    }
	    }
	    else {
		    if (!Input::isDown(LEFT_MOUSE_BUTTON)) {
		      enterMainMenuState();
          primed_ = false;
		    }
	    }
      break;

    default:
      ;
    }
  }

  void drawBackground(shader &shader, texture* tex, mat4 &modelToProjection) {

    if (distanceTravelled_ <= 2400) {
      buildingSprite_.draw(vec4(0, 160 - FRAME_HEIGHT / 2 - (float)distanceTravelled_ / 9, 0, 0), true, shader, tex, modelToProjection);
    }
    else {
      // if dist > some number, draw starry background.
    }
  }

  void drawLives(shader &shader, texture* tex, mat4 &modelToProjection) {

    float x = 20;
    float y = 20;
    float next = 20;
    
    int lives = StarManager::get().getLives();

    for (int i = 0; i != lives; ++i) {
      vec4 position((x + next * i) - ((float)FRAME_WIDTH / 2), ((float)FRAME_HEIGHT / 2) - y, 0, 1);
      balloonIconSprite_.draw(position, true, shader, tex, modelToProjection);
    }    
  }

  void drawCurrentScore(shader &shader, texture* tex, mat4 &modelToProjection) {
    int x = StarManager::get().getScore();

    glEnable(GL_BLEND);

    float distance_between_digit_centres = scoreDigitHalfWidth_ * 1.5f;
    vec4 current_digit_centre = vec4((FRAME_WIDTH / 2 - 10 - scoreDigitHalfWidth_), (FRAME_HEIGHT / 2 - 10 - scoreDigitHalfHeight_), 0, 1);

    if (x == 0) {
      tex[0].render(0);

      shader.render(modelToProjection);
      
      
      float vertices[4*4] = {
        current_digit_centre[0] - scoreDigitHalfWidth_, current_digit_centre[1] - scoreDigitHalfHeight_, 0, 0,
        current_digit_centre[0] + scoreDigitHalfWidth_, current_digit_centre[1] - scoreDigitHalfHeight_, 1, 0,
        current_digit_centre[0] + scoreDigitHalfWidth_, current_digit_centre[1] + scoreDigitHalfHeight_, 1, 1,
        current_digit_centre[0] - scoreDigitHalfWidth_, current_digit_centre[1] + scoreDigitHalfHeight_, 0, 1,
      };

      glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)vertices );
      glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)(vertices + 2) );
      glEnableVertexAttribArray(0);
      glEnableVertexAttribArray(2);
      
      glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
    }

    else {

      while( x > 0)
      {
        tex[x % 10].render(0);

        shader.render(modelToProjection);
      
        // Draw Last digit of the number (x);
        float vertices[4*4] = {
          current_digit_centre[0] - scoreDigitHalfWidth_, current_digit_centre[1] - scoreDigitHalfHeight_, 0, 0,
          current_digit_centre[0] + scoreDigitHalfWidth_, current_digit_centre[1] - scoreDigitHalfHeight_, 1, 0,
          current_digit_centre[0] + scoreDigitHalfWidth_, current_digit_centre[1] + scoreDigitHalfHeight_, 1, 1,
          current_digit_centre[0] - scoreDigitHalfWidth_, current_digit_centre[1] + scoreDigitHalfHeight_, 0, 1,
        };

        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)vertices );
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)(vertices + 2) );
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(2);

        glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

	      x = x/10;
        current_digit_centre[0] -= distance_between_digit_centres;
      }

    }

    glDisable(GL_BLEND);
  }


  void drawGameOver(shader &shader, texture* tex, mat4 &modelToProjection) {
    gameOverSprite_.draw(vec4(0, 0, 0, 1), false, shader, tex, modelToProjection);
    
    // draw final score
    int x = StarManager::get().getScore();

    glEnable(GL_BLEND);
    

    float distance_between_digit_centres = scoreDigitHalfWidth_ * 1.5f;
    vec4 current_digit_centre = vec4(5, 0, 0, 1);

    if (x == 0) {
      tex[0].render(0);

      shader.render(modelToProjection);
      
      
      float vertices[4*4] = {
        current_digit_centre[0] - scoreDigitHalfWidth_, current_digit_centre[1] - scoreDigitHalfHeight_, 0, 0,
        current_digit_centre[0] + scoreDigitHalfWidth_, current_digit_centre[1] - scoreDigitHalfHeight_, 1, 0,
        current_digit_centre[0] + scoreDigitHalfWidth_, current_digit_centre[1] + scoreDigitHalfHeight_, 1, 1,
        current_digit_centre[0] - scoreDigitHalfWidth_, current_digit_centre[1] + scoreDigitHalfHeight_, 0, 1,
      };

      glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)vertices );
      glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)(vertices + 2) );
      glEnableVertexAttribArray(0);
      glEnableVertexAttribArray(2);
      
      glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
    }

    else {

      while( x > 0)
      {
        tex[x % 10].render(0);

        shader.render(modelToProjection);
      
        // Draw Last digit of the number (x);
        float vertices[4*4] = {
          current_digit_centre[0] - scoreDigitHalfWidth_, current_digit_centre[1] - scoreDigitHalfHeight_, 0, 0,
          current_digit_centre[0] + scoreDigitHalfWidth_, current_digit_centre[1] - scoreDigitHalfHeight_, 1, 0,
          current_digit_centre[0] + scoreDigitHalfWidth_, current_digit_centre[1] + scoreDigitHalfHeight_, 1, 1,
          current_digit_centre[0] - scoreDigitHalfWidth_, current_digit_centre[1] + scoreDigitHalfHeight_, 0, 1,
        };

        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)vertices );
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)(vertices + 2) );
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(2);

        glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

	      x = x/10;
        current_digit_centre[0] -= distance_between_digit_centres;
      }

    }

    glDisable(GL_BLEND);


  }

  void drawHighScores(shader &shader, texture* tex, mat4 &modelToProjection) {
    highScoresSprite_.draw(vec4(0, 0, 0, 1), false, shader, tex, modelToProjection);
    
    glEnable(GL_BLEND);

    for (int i = 0; i != 10; ++i) {


        int x = topTen_[i];


        float distance_between_digit_centres = scoreDigitHalfWidth_ * 1.5f;
        vec4 current_digit_centre = vec4(25, 145 - (i * 30), 0, 1);

        if (x == 0) {
          tex[0].render(0);

          shader.render(modelToProjection);
      
      
          float vertices[4*4] = {
            current_digit_centre[0] - scoreDigitHalfWidth_, current_digit_centre[1] - scoreDigitHalfHeight_, 0, 0,
            current_digit_centre[0] + scoreDigitHalfWidth_, current_digit_centre[1] - scoreDigitHalfHeight_, 1, 0,
            current_digit_centre[0] + scoreDigitHalfWidth_, current_digit_centre[1] + scoreDigitHalfHeight_, 1, 1,
            current_digit_centre[0] - scoreDigitHalfWidth_, current_digit_centre[1] + scoreDigitHalfHeight_, 0, 1,
          };

          glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)vertices );
          glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)(vertices + 2) );
          glEnableVertexAttribArray(0);
          glEnableVertexAttribArray(2);
      
          glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
        }

        else {

          while( x > 0)
          {
            tex[x % 10].render(0);

            shader.render(modelToProjection);
      
            // Draw Last digit of the number (x);
            float vertices[4*4] = {
              current_digit_centre[0] - scoreDigitHalfWidth_, current_digit_centre[1] - scoreDigitHalfHeight_, 0, 0,
              current_digit_centre[0] + scoreDigitHalfWidth_, current_digit_centre[1] - scoreDigitHalfHeight_, 1, 0,
              current_digit_centre[0] + scoreDigitHalfWidth_, current_digit_centre[1] + scoreDigitHalfHeight_, 1, 1,
              current_digit_centre[0] - scoreDigitHalfWidth_, current_digit_centre[1] + scoreDigitHalfHeight_, 0, 1,
            };

            glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)vertices );
            glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)(vertices + 2) );
            glEnableVertexAttribArray(0);
            glEnableVertexAttribArray(2);

            glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

	          x = x/10;
            current_digit_centre[0] -= distance_between_digit_centres;
          }

        }

    }

    glDisable(GL_BLEND);


  }

  void draw(shader &shader, mat4 &modelToProjection) {
    
    switch(state_) {
    case STATE_SPLASH:
      splashScreenSprite_.draw(vec4(0, 0, 0, 1), false, shader, textures, modelToProjection);
      break;

    case STATE_MAIN_MENU:
      mainMenuSprite_.draw(vec4(0, 0, 0, 1), false, shader, textures, modelToProjection);
      Input::drawVisibleButtons(shader, textures, modelToProjection);
      break;

    case STATE_INSTRUCTIONS:
      instructionsSprite_.draw(vec4(0, 0, 0, 1), false, shader, textures, modelToProjection);
      break;

    case STATE_MAIN_GAME:
      CloudManager::get().draw(shader, textures, modelToProjection);
      drawBackground(shader, textures, modelToProjection);
      StarManager::get().draw(shader, textures, modelToProjection);
      balloon_.draw(shader, textures, modelToProjection);
      StarManager::get().drawMiniStars(shader, textures, modelToProjection);

      if (distanceTravelled_ <= 500) {
        grassSprite_.draw(vec4(0, 128 - FRAME_HEIGHT / 2 - (float)distanceTravelled_ / 3, 0 , 1), true, shader, textures, modelToProjection);
      }

      // GUI stuff
      drawCurrentScore(shader, textures, modelToProjection);
      drawLives(shader, textures, modelToProjection);
      break;

    case STATE_GAME_OVER:
      drawGameOver(shader, textures, modelToProjection);
      break;

    case STATE_HIGH_SCORES:
      drawHighScores(shader, textures, modelToProjection);
      break;

    default:
      ;
    }
  }
  
  shader colourShader_;
  GLint viewportWidth_;
  GLint viewportHeight_;
  mat4 modelToProjection_;
  
  // boiler plate for building a camera
  void buildCameraMatrix()
  {
    // this matrix can be used to move objects around in the scene
    mat4 modelToWorld;
    modelToWorld.loadIdentity();

    // this is the transform from the camera to the world
    mat4 cameraToWorld;
    cameraToWorld.loadIdentity();
    cameraToWorld.translate(0, 0, 10);

    // flip it around to transform from world to camera
    mat4 worldToCamera;
    cameraToWorld.invertQuick(worldToCamera);

    // build a projections matrix to add perspective
    mat4 cameraToProjection;
    cameraToProjection.loadIdentity();
    float zNear = 10;
    float zFar = 30;

    GLfloat left   = (float)-viewportWidth_ / 2;
    GLfloat right  = (float) viewportWidth_ / 2;
    GLfloat bottom = (float)-viewportWidth_ / 2;
    GLfloat top    = (float) viewportWidth_ / 2;
    
    float aspectRatio = (float)viewportWidth_ / (float)viewportHeight_;
    
    if ( aspectRatio < 1.0f ) { // window taller than wide
      bottom /= aspectRatio;
      top /= aspectRatio;
    } else {
      left *= aspectRatio;
      right *= aspectRatio;
    }    
    
    cameraToProjection.frustum(left, right, bottom, top, zNear, zFar);

    // model -> world -> camera -> projection
    modelToProjection_ = modelToWorld * worldToCamera * cameraToProjection;
  }
  
  void render()
  {
    update();

    // clear the frame buffer and the depth
    glClearColor(backgroundColour[0], backgroundColour[1], backgroundColour[2], backgroundColour[3]);
    glViewport(0, 0, viewportWidth_, viewportHeight_);
    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);    

    draw(colourShader_, modelToProjection_);

    // swap buffers so that the image is displayed.
    // gets a new buffer to draw a new scene.
    glutSwapBuffers();
  }

  void setupButtons() {
    Input::initButton(GUI_BUTTON_START,        TX_START_BUTTON,        TX_START_BUTTON_OVER,        vec4(  0,    0, 0, 1), vec4(64, 32, 0, 0));
    Input::initButton(GUI_BUTTON_HIGH_SCORES,  TX_HIGH_SCORES_BUTTON,  TX_HIGH_SCORES_BUTTON_OVER,  vec4(  0,  -70, 0, 1), vec4(64, 32, 0, 0));
    Input::initButton(GUI_BUTTON_SOUND_IS_ON,  TX_SOUND_IS_ON,         TX_SOUND_IS_ON_OVER,         vec4(100, -180, 0, 1), vec4(16, 16, 0, 0));
    Input::initButton(GUI_BUTTON_SOUND_IS_OFF, TX_SOUND_IS_OFF,        TX_SOUND_IS_OFF_OVER,        vec4(100, -180, 0, 1), vec4(16, 16, 0, 0));
    Input::initButton(GUI_BUTTON_INSTRUCTIONS, TX_INSTRUCTIONS_BUTTON, TX_INSTRUCTIONS_BUTTON_OVER, vec4(  0,   70, 0, 1), vec4(64, 32, 0, 0));
  }

  Game()
  {
    // set up a simple shader to render the emissve color
    colourShader_.init(
      // just copy the position attribute to gl_Position
      "varying vec2 uv_;"
      "attribute vec4 pos;"
      "attribute vec2 uv;"
      "uniform mat4 modelToProjection;"
      "void main() {"
      "  gl_Position = modelToProjection * pos;"
      "  uv_ = uv;"
      "}",

      // just copy the color attribute to gl_FragColor
      "varying vec2 uv_;"
      "uniform sampler2D texture;"

      "void main() {"
      "  gl_FragColor = texture2D(texture, uv_);"
      "}"
    );

    soundIsOn_= true;

    loadTextures();
    sound_manager::add_buffers("assets/labels.txt", "assets/sfx.wav");
    setupButtons();

    vec4 screenHalfExtents(FRAME_WIDTH / 2, FRAME_HEIGHT / 2, 0, 0);

    buildingSprite_ = Sprite(TX_BUILDING, vec4(160, 160, 0, 0));
    grassSprite_ = Sprite(TX_FOREGRASS, vec4(160, 128, 0, 0));
    splashScreenSprite_ = Sprite(TX_SPLASH_SCREEN, screenHalfExtents);
    mainMenuSprite_ = Sprite(TX_MAIN_MENU, screenHalfExtents);
    gameOverSprite_ = Sprite(TX_GAME_OVER, screenHalfExtents);
    highScoresSprite_ = Sprite(TX_HIGH_SCORES, screenHalfExtents);
    instructionsSprite_ = Sprite(TX_INSTRUCTIONS, screenHalfExtents);
    balloonIconSprite_ = Sprite(TX_BALLOON_ICON, vec4(10, 10, 0, 0));

    setViewport(FRAME_WIDTH, FRAME_HEIGHT);
    buildCameraMatrix();

    topTen_[0] = 500;
    topTen_[1] = 450;
    topTen_[2] = 400;
    topTen_[3] = 350;
    topTen_[4] = 300;
    topTen_[5] = 250;
    topTen_[6] = 200;
    topTen_[7] = 150;
    topTen_[8] = 100;
    topTen_[9] =  50;
  }

  // The viewport defines the drawing area in the window
  void setViewport(int w, int h) {
    viewportWidth_ = w;
    viewportHeight_ = h;

    widthRatio_ = (float)w / (float)FRAME_WIDTH;
    heightRatio_ = (float)h / (float)FRAME_HEIGHT;
  }

  int translateScreenXtoViewX(int x) {
    float tx = (x - (get().viewportWidth_ / 2) ) / get().widthRatio_;
    return (int)tx;
  }

  int translateScreenYtoViewY(int y) {
    float ty = (get().viewportHeight_ / 2 - y) / get().heightRatio_;
    return (int)ty;
  }

public:
  // a singleton: one instance of this class only!
  static Game &get()
  {
    static Game singleton;
    return singleton;
  }

  // interface from GLUT
  static void reshape(int w, int h) {
    get().setViewport(w, h);
  }
  static void display() { get().render(); }

  static void mouse(int button, int state, int x, int y) {
    Input::get().setMousePosition(get().translateScreenXtoViewX(x), get().translateScreenYtoViewY(y));
    
    if (state == GLUT_DOWN) {
      if (button == GLUT_LEFT_BUTTON) {
        Input::get().press(LEFT_MOUSE_BUTTON);
      }
    } else {
      if (button == GLUT_LEFT_BUTTON) {
        Input::get().release(LEFT_MOUSE_BUTTON);
      }
    }
  }

  static void motion(int x, int y) {
    Input::get().setMousePosition(get().translateScreenXtoViewX(x), get().translateScreenYtoViewY(y));
  }

  static void timer(int value) { glutTimerFunc(30, timer, 1); glutPostRedisplay(); }
};

